﻿using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Threading;

namespace System.Linq.Reactive
{
    /// <summary>Методы-расширения интерфейса наблюдаемых объектов <see cref="IObservableEx{T}"/></summary>
    public static class ObservableEx
    {
        /// <summary>Метод получения наблюдаемого объекта для объекта, реализующего интерфейс <see cref="INotifyPropertyChanged"/>  для указанного имени свойства</summary>
        /// <typeparam name="T">Тип значения свойства</typeparam>
        /// <param name="obj">Наблюдаемый объект</param>
        /// <param name="ProperyName">Имя свойства</param>
        /// <returns>Объект-наблюдатель за свойством</returns>
        public static IObservableEx<T> FromProperty<T>(this INotifyPropertyChanged obj, string ProperyName)
        {
            Contract.Requires(obj != null);
            Contract.Requires(ProperyName != null);
            Contract.Requires(ProperyName != string.Empty);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            return new Property<T>(obj, ProperyName);
        }

        /// <summary>Метод-лианиризатор событий для наблюдаемого объекта, возвращающего коллекцию объектов типа <see cref="T"/></summary>
        /// <typeparam name="T">Тип результирующих объектов</typeparam>
        /// <param name="o">Объект-наблюдатель коллекции</param>
        /// <returns>Объект-наблюдатель элементов коллекции</returns>
        public static IObservableEx<T> SelectMany<T>(this IObservableEx<IEnumerable<T>> o)
        {
            Contract.Requires(o != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            var result = new SimpleObservableEx<T>();
            o.ForeachAction(t => t.Foreach(result.OnNext));
            o.OnComplited(result.OnComplited);
            o.OnException(result.OnException);
            return result;
        }

        /// <summary>Метод фильтрации событий</summary>
        /// <typeparam name="T">Тип объектов событий</typeparam>
        /// <param name="observable">Исходный объект-наблюдатель</param>
        /// <param name="Predicate">Метод фильтрации</param>
        /// <returns>Объекнаблюдатель с установленным методом фильтрации</returns>
        public static IObservableEx<T> WhereAction<T>(this IObservableEx<T> observable, Func<T, bool> Predicate)
        {
            Contract.Requires(observable != null);
            Contract.Requires(Predicate != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            return new WhereLamdaObservableEx<T>(observable.GetObserver(), Predicate);
        }

        /// <summary>Метод фильтрации событий</summary>
        /// <typeparam name="T">Тип объектов событий</typeparam>
        /// <param name="observable">Исходный объект-наблюдатель</param>
        /// <param name="Predicate">Метод фильтрации</param>
        /// <param name="ElseAction">Метод обработки невошедших событий</param>
        /// <returns>Объекнаблюдатель с установленным методом фильтрации</returns>
        public static IObservableEx<T> WhereAction<T>(this IObservableEx<T> observable, Func<T, bool> Predicate, Action<T> ElseAction)
        {
            Contract.Requires(observable != null);
            Contract.Requires(Predicate != null);
            Contract.Requires(ElseAction != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            Func<T, bool> predicate = t =>
            {
                var result = Predicate(t);
                if(!result) ElseAction(t);
                return result;
            };

            return new WhereLamdaObservableEx<T>(observable.GetObserver(), predicate);
        }

        /// <summary>Метод преобразования объектов событий</summary>
        /// <typeparam name="T">Тип исходных объектов событий</typeparam>
        /// <typeparam name="Q">Тип результирующих объектов событий</typeparam>
        /// <param name="observable">Исходный объект-наблюдатель</param>
        /// <param name="Selector">Объект-наблюдатель с преобразованными объектами событий</param>
        /// <returns>Объект-наблюдатель с преобразованными типами объектов</returns>
        public static IObservableEx<Q> SelectAction<T, Q>(this IObservableEx<T> observable, Func<T, Q> Selector)
        {
            Contract.Requires(observable != null);
            Contract.Requires(Selector != null);
            Contract.Ensures(Contract.Result<IObservableEx<Q>>() != null);
            return new SelectLamdaObservableEx<T, Q>(observable.GetObserver(), Selector);
        }

        public static TimeIntervalObservable Interval_Seconds(double TimeInterval, bool Started = false)
        {
            Contract.Requires(TimeInterval >= 0);
            Contract.Ensures(Contract.Result<TimeIntervalObservable>() != null);
            return Interval(TimeSpan.FromSeconds(TimeInterval), Started);
        }

        public static TimeIntervalObservable Interval_MiliSeconds(double TimeInterval, bool Started = false)
        {
            Contract.Requires(TimeInterval >= 0);
            Contract.Ensures(Contract.Result<TimeIntervalObservable>() != null);
            return Interval(TimeSpan.FromMilliseconds(TimeInterval), Started);
        }

        public static TimeIntervalObservable Interval_Minutes(double TimeInterval, bool Started = false)
        {
            Contract.Requires(TimeInterval >= 0);
            Contract.Ensures(Contract.Result<TimeIntervalObservable>() != null);
            return Interval(TimeSpan.FromMinutes(TimeInterval), Started);
        }

        public static TimeIntervalObservable Interval_Hours(double TimeInterval, bool Started = false)
        {
            Contract.Requires(TimeInterval >= 0);
            Contract.Ensures(Contract.Result<TimeIntervalObservable>() != null);
            return Interval(TimeSpan.FromHours(TimeInterval), Started);
        }

        public static TimeIntervalObservable Interval(TimeSpan TimeInterval, bool Started = false)
        {
            Contract.Ensures(Contract.Result<TimeIntervalObservable>() != null);
            return new TimeIntervalObservable(TimeInterval, Started);
        }

        /// <summary>Метод получения синхронно задержанных во времени событий</summary>
        /// <typeparam name="T">Тип объектов событий</typeparam>
        /// <param name="Observable">Исходный объект-наблюдатель</param>
        /// <param name="Interval">Интервал времени задержки событий</param>
        /// <returns>ОБъект-наблюдатель, события которого синхронно задержаны во времени на указанный интервал</returns>
        public static IObservableEx<T> Whait<T>(this IObservableEx<T> Observable, TimeSpan Interval)
        {
            Contract.Requires(Observable != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            return new LamdaObservable<T>(Observable.GetObserver(), (o, t) => { Thread.Sleep(Interval); o.OnNext(t); });
        }

        /// <summary>Метод получения задержанных во времени событий</summary>
        /// <typeparam name="T">Тип объектов событий</typeparam>
        /// <param name="Observable">Исходный объект-наблюдатель</param>
        /// <param name="Interval">Интервал времени задержки событий</param>
        /// <returns>ОБъект-наблюдатель, события которого задержаны во времени на указанный интервал</returns>
        public static IObservableEx<T> WhaitAsync<T>(this IObservableEx<T> Observable, TimeSpan Interval)
        {
            Contract.Requires(Observable != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            Action<IObserverEx<T>, T> OnNext = (o, t) => { Thread.Sleep(Interval); o.OnNext(t); };
            Action<IObserverEx<T>, T> OnNextAsync = (o, t) => OnNext.BeginInvoke(o, t, null, null);
            return new LamdaObservable<T>(Observable.GetObserver(), OnNextAsync);
        }

        //public static IObservableEx<TEventArgs> FromEvent<TEventArgs>(this object Obj, string EventName)
        //    where TEventArgs : EventArgs
        //{
        //    return new EventObservableEx<TEventArgs>(Obj, EventName);
        //}

        /// <summary>Метод получения объекта-наблюдателя для указанного события</summary>
        /// <typeparam name="TEventArgs">Тип аргументов события</typeparam>
        /// <param name="Obj">Наблюдаемый объект</param>
        /// <param name="EventName">Имя события</param>
        /// <returns>Объект-ниблюдатель за событием</returns>
        public static IObservableEx<TEventArgs> FromEvent<TEventArgs>(this object Obj, string EventName)
            where TEventArgs : EventArgs
        {
            Contract.Requires(Obj != null);
            Contract.Requires(EventName != null);
            Contract.Requires(EventName != string.Empty);
            Contract.Ensures(Contract.Result<IObservableEx<TEventArgs>>() != null);
            return new EventObservable2Ex<TEventArgs>(Obj, EventName);
        }

        /// <summary>Метод получения объекта-наблюдателя из объекта-перечисления</summary>
        /// <typeparam name="T">Тип объектов перечисления</typeparam>
        /// <param name="collection">Перечисление объектов</param>
        /// <param name="observable">Созданный объект-наблюдатель за перечислением объектов коллекции</param>
        /// <returns>Новое перечисление объектов, перечисление объектов которого вызывает события в наблюдателе</returns>
        public static IEnumerable<T> GetObservable<T>(this IEnumerable<T> collection, out IObservableEx<T> observable)
        {
            Contract.Requires(collection != null);
            Contract.Ensures(Contract.ValueAtReturn(out observable) != null);
            Contract.Ensures(Contract.Result<IEnumerable<T>>() != null);
            observable = new SimpleObservableEx<T>();
            return collection
                .ForeachLeazy(((SimpleObservableEx<T>)observable).OnNext)
                .OnComplite(((SimpleObservableEx<T>)observable).OnComplited);
        }

        /// <summary>Метод получения объекта-нибулюдателя, пропускающего после создания указанное число событий</summary>
        /// <typeparam name="T">Тип объектов события</typeparam>
        /// <param name="observable">Исходный объект-наблюдатель</param>
        /// <param name="Count">Количество пропускаемых событий</param>
        /// <returns>Объект-наблюдатель с указанным количеством пропускаемых событий</returns>
        public static IObservableEx<T> TakeAction<T>(this IObservableEx<T> observable, int Count)
        {
            Contract.Requires(observable != null);
            Contract.Requires(Count >= 0);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            return new TakeObservable<T>(observable.GetObserver(), Count);
        }

        /// <summary>Метод обработки последовательности событий с учётом разрешающей и запрещающей последовательностей</summary>
        /// <typeparam name="T">Тип объектов событий наблюдаемого объекта</typeparam>
        /// <typeparam name="O">Тип объектов событий разрешающей последовательности</typeparam>
        /// <typeparam name="C">Тип объектов событий запрещающей последовательности</typeparam>
        /// <param name="source">Объект-наблюдатель источник событий</param>
        /// <param name="Open">Объект-наблюдатель разрешающий событий в выходной последовательности</param>
        /// <param name="Close">Объект-наблюдатель запрещающий событий в выходной последовательности</param>
        /// <param name="IsOpen">Исходное состояния разрешения событий в выходной последовательности (по умолчанию разрешено)</param>
        /// <returns>Управляемый объект-наблюдатель</returns>
        public static IObservableEx<T> TakeAction<T, O, C>(this IObservableEx<T> source, IObservableEx<O> Open, IObservableEx<C> Close, bool IsOpen = true)
        {
            Contract.Requires(source != null);
            Contract.Requires(Open != null);
            Contract.Requires(Close != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            var t = source as TriggeredObservable<T> ?? new TriggeredObservable<T>(source.GetObserver(), IsOpen);
            Open.ForeachAction(o => t.Open = true);
            Close.ForeachAction(c => t.Open = false);
            return t;
        }

        /// <summary>Метод получения объекта-наблюдателя, события в котором пропускаются в выходную последовательность только до появления события в управляющей последовательности</summary>
        /// <typeparam name="T">Тип исходных объектов события</typeparam>
        /// <typeparam name="Q">Тип события управляющей последовательности</typeparam>
        /// <param name="source">Исходный объект-наблюдатель</param>
        /// <param name="Selector">Обект-наблюдатель управляющей последовательности</param>
        /// <param name="IsOpen">Исходное состояние выходной последовательности</param>
        /// <returns>Объект-наблюдатель управляемый управляющей последовательностью</returns>
        public static IObservableEx<T> TakeUntil<T, Q>(this IObservableEx<T> source, IObservableEx<Q> Selector, bool IsOpen = true)
        {
            Contract.Requires(source != null);
            Contract.Requires(Selector != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            var o = source as TriggeredObservable<T> ?? new TriggeredObservable<T>(source.GetObserver(), IsOpen);
            Selector.ForeachAction(q => o.Open = false);
            return o;
        }

        /// <summary>Метод получения объекта-наблюдателя, события в котором пропускаются в выходную последовательность только после появления события в управляющей последовательности</summary>
        /// <typeparam name="T">Тип исходных объектов события</typeparam>
        /// <typeparam name="Q">Тип события управляющей последовательности</typeparam>
        /// <param name="source">Исходный объект-наблюдатель</param>
        /// <param name="Selector">Обект-наблюдатель управляющей последовательности</param>
        /// <param name="IsOpen">Исходное состояние выходной последовательности</param>
        /// <returns>Объект-наблюдатель управляемый управляющей последовательностью</returns>
        public static IObservableEx<T> SkipWhile<T, Q>(this IObservableEx<T> source, IObservableEx<Q> Selector, bool IsOpen = false)
        {
            Contract.Requires(source != null);
            Contract.Requires(Selector != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            var o = source as TriggeredObservable<T> ?? new TriggeredObservable<T>(source.GetObserver(), IsOpen);
            Selector.ForeachAction(q => o.Open = true);
            return o;
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Next"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="action">Метод обработки события <see cref="IObserverEx{T}.Next"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> ForeachAction<T>(this IObservableEx<T> observable, Action<T> action)
        {
            Contract.Requires(observable != null);
            Contract.Requires(action != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            return observable.InitializeObject(o => o.GetObserver().Next += action);
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Next"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="action">Метод обработки события <see cref="IObserverEx{T}.Next"/></param>
        /// <param name="where">Метод выборки события <see cref="IObserverEx{T}.Next"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> ForeachAction<T>(this IObservableEx<T> observable, Action<T> action, Func<T, bool> where)
        {
            Contract.Requires(observable != null);
            Contract.Requires(action != null);
            Contract.Requires(where != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            return observable.InitializeObject(o => o.GetObserver().Next += t => { if(where(t)) action(t); });
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Next"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="action">Метод обработки события <see cref="IObserverEx{T}.Next"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> ForeachAction<T>(this IObservableEx<T> observable, Action<T, int> action)
        {
            Contract.Requires(observable != null);
            Contract.Requires(action != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            var i = 0;
            observable.GetObserver().Next += t => action(t, i++);
            return observable;
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Next"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="action">Метод обработки события <see cref="IObserverEx{T}.Next"/></param>
        /// <param name="where">Метод выборки события <see cref="IObserverEx{T}.Next"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> ForeachAction<T>(this IObservableEx<T> observable, Action<T, int> action, Func<T, int, bool> where)
        {
            Contract.Requires(observable != null);
            Contract.Requires(action != null);
            Contract.Requires(where != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            var i = 0;
            observable.GetObserver().Next += t => { if(where(t, i)) action(t, i++); };
            return observable;
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Exception"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="OnError">Метод обработки события <see cref="IObserverEx{T}.Exception"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> OnException<T>(this IObservableEx<T> observable, Action<Exception> OnError)
        {
            Contract.Requires(observable != null);
            Contract.Requires(OnError != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            observable.GetObserver().Exception += OnError;
            return observable;
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Complited"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="OnComplited">Метод обработки события <see cref="IObserverEx{T}.Complited"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> OnComplited<T>(this IObservableEx<T> observable, Action OnComplited)
        {
            Contract.Requires(observable != null);
            Contract.Requires(OnComplited != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            observable.GetObserver().Complited += OnComplited;
            return observable;
        }

        /// <summary>Метод обработки события <see cref="IObserverEx{T}.Reset"/></summary>
        /// <typeparam name="T">Тип объектов наблюдения</typeparam>
        /// <param name="observable">Исходный обект-наблюдатель</param>
        /// <param name="OnReset">Метод обработки события <see cref="IObserverEx{T}.Reset"/></param>
        /// <returns>Исходный объект-наблюдатель</returns>
        public static IObservableEx<T> OnReset<T>(this IObservableEx<T> observable, Action OnReset)
        {
            Contract.Requires(observable != null);
            Contract.Requires(OnReset != null);
            Contract.Ensures(Contract.Result<IObservableEx<T>>() != null);
            observable.GetObserver().Reset += OnReset;
            return observable;
        }
    }
}